﻿#region Copyright(C) 2018 FI Ltd. All rights reserved.
// ==================================================================================================
// Copyright(C) 2018 FI Ltd. All rights reserved.
//
// SYSTEM NAME	    : FI.Net Application Framework
// COMPONENT ID	    : FI.Core
// COMPONENT DESC   : The foundation component of the whole framework. 	
//
// CREATED DATE/BY  : 18 Aug 2018 / Tian Mi
//
// REVISION HISTORY :	
// DATE/BY			SR#/CS/PM#/OTHERS		DESCRIPTION OF CHANGE
// ==================================================================================================
#endregion

using FI.Core.Properties;
using FI.Core.Utilities;
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;

namespace FI.Core
{
    /// <summary>
    /// The PropertyAccessor is used to set or get the property value.
    /// </summary>
    /// <remarks>The underlying mechanism is based on IL Emit, which provides better performance than pure reflection.</remarks>
    public class PropertyAccessor
    {
        #region Private Static Fields
        private static readonly Dictionary<PropertyAccessorKey, PropertyAccessor> propertyAccessors = new Dictionary<PropertyAccessorKey, PropertyAccessor>();
        private static readonly object synchHelper = new object();
        private PropertyInfo _property;
        #endregion

        #region Public Properties
        /// <summary>
        /// Gets or sets the type of the target.
        /// </summary>
        /// <value>The type of the target.</value>
        public Type TargetType { get; private set; }

        /// <summary>
        /// Gets or sets the name of the property.
        /// </summary>
        /// <value>The name of the property.</value>
        public string PropertyName { get; private set; }

        /// <summary>
        /// Gets or sets the type of the property.
        /// </summary>
        /// <value>The type of the property.</value>
        public Type PropertyType { get; private set; }

        /// <summary>
        /// Gets or sets the delegate to get property value.
        /// </summary>
        /// <value>The delegate to get property value.</value>
        public Func<object, object> GetFunction { get; private set; }

        /// <summary>
        /// Gets or sets the delegate to set property value.
        /// </summary>
        /// <value>The delegate to set property value.</value>
        public Action<object, object> SetAction { get; private set; }

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the PropertyAccessor&lt;T&gt; class.
        /// </summary>
        /// <param name="targetType">Type of the target.</param>
        /// <param name="propertyName">Name of the property.</param>
        public PropertyAccessor(Type targetType, string propertyName)
        {
            Guard.ArgumentNotNull(targetType, "targetType");
            Guard.ArgumentNotNullOrEmpty(propertyName, "propertyName");

            this._property = targetType.GetProperty(propertyName);
            if (null == _property)
            {
                throw new ArgumentException(ResourceUtility.Format(Resources.ExceptionPropertyNotExists, propertyName, targetType.FullName));
            }
            this.TargetType     = targetType;
            this.PropertyName   = propertyName;
            this.PropertyType   = _property.PropertyType;
            if (_property.CanRead)
            {
                this.GetFunction = CreateGetFunction();
            }
            if (_property.CanWrite)
            {
                this.SetAction = CreateSetAction();
            }
        }
        #endregion

        #region Public Instance Methods

        /// <summary>
        /// Gets the property value of the given object.
        /// </summary>
        /// <param name="target">The target object.</param>
        /// <returns>The property value of the given object.</returns>
        public object Get(object target)
        {
            Guard.ArgumentNotNull(target, "target");
            if (!this.TargetType.IsAssignableFrom(target.GetType()))
            {
                throw new ArgumentException(Resources.ExceptionNotAssignableFrom, "target");
            }
            if (null == this.GetFunction)
            {
                throw new InvalidOperationException(ResourceUtility.Format(Resources.ExceptionPropertyNotReadable, this.PropertyName, this.TargetType.FullName));
            }
            return this.GetFunction(target);
        }

        /// <summary>
        /// Sets the property value of the given object.
        /// </summary>
        /// <param name="target">The target object.</param>
        /// <param name="value">The property value.</param>
        public void Set(object target, object value)
        {
            Guard.ArgumentNotNull(target, "obj");
            if (null == this.SetAction)
            {
                throw new InvalidOperationException(ResourceUtility.Format(Resources.ExceptionPropertyNotWritable, this.PropertyName, this.TargetType.FullName));
            }
            if (!this.TargetType.IsAssignableFrom(target.GetType()))
            {
                throw new ArgumentException(Resources.ExceptionNotAssignableFrom, "value");
            }
            this.SetAction(target, value);
        }

        #endregion

        #region Public Static Methods
        /// <summary>
        /// Gets the property value of the given object.
        /// </summary>
        /// <param name="target">The target object.</param>
        /// <param name="propertyName">Name of the property.</param>
        /// <returns>The property value of the given object.</returns>
        public static object Get(object target, string propertyName)
        {
            Guard.ArgumentNotNull(target, "obj");
            Guard.ArgumentNotNullOrEmpty(propertyName, "propertyName");
            var propertyAccessor = LoadOrCreatePropertyAccessor(target.GetType(), propertyName);
            return propertyAccessor.Get(target);
        }

        /// <summary>
        /// Sets the property value of the given object.
        /// </summary>
        /// <param name="target">The target object.</param>
        /// <param name="propertyName">Name of the property.</param>
        /// <param name="value">The property value.</param>
        public static void Set(object target, string propertyName, object value)
        {
            Guard.ArgumentNotNull(target, "obj");
            Guard.ArgumentNotNullOrEmpty(propertyName, "propertyName");
            var propertyAccessor = LoadOrCreatePropertyAccessor(target.GetType(), propertyName);
            propertyAccessor.Set(target,value);
        }

        /// <summary>
        /// Gets the type of the property.
        /// </summary>
        /// <param name="targetType">Type of the target.</param>
        /// <param name="propertyName">Name of the property.</param>
        /// <returns>The type of the given property.</returns>
        public static Type GetPropertyType(Type targetType, string propertyName)
        {
            Guard.ArgumentNotNull(targetType, "targetType");
            Guard.ArgumentNotNullOrEmpty(propertyName, "propertyName");
            var propertyAccessor = LoadOrCreatePropertyAccessor(targetType, propertyName);
            return propertyAccessor.PropertyType;
        }

        #endregion

        #region Private Methods

        private Func<object, object> CreateGetFunction()
        {
            var getMethod           = this._property.GetGetMethod();
            var target              = Expression.Parameter(typeof(object), "target");
            var castedTarget        = getMethod.IsStatic ? null : Expression.Convert(target, this.TargetType);
            var getProperty         = Expression.Property(castedTarget, _property);
            var castPropertyValue   = Expression.Convert(getProperty, typeof(object));
            return Expression.Lambda<Func<object, object>>(castPropertyValue, target).Compile();
        }

        private Action<object, object> CreateSetAction()
        {
            var setMethod           = this._property.GetSetMethod();
            var target              = Expression.Parameter(typeof(object), "target");
            var propertyValue       = Expression.Parameter(typeof(object), "value");
            var castedTarget        = setMethod.IsStatic ? null : Expression.Convert(target, this.TargetType);
            var castedpropertyValue = Expression.Convert(propertyValue, this.PropertyType);
            var propertySet         = Expression.Call(castedTarget, setMethod, castedpropertyValue);
            return Expression.Lambda<Action<object, object>>(propertySet, target, propertyValue).Compile();         
        }

        private static PropertyAccessor LoadOrCreatePropertyAccessor(Type targetType, string propertyName)
        {           
            PropertyAccessorKey key = new PropertyAccessorKey(targetType, propertyName);
            if (propertyAccessors.ContainsKey(key))
            {
                return  propertyAccessors[key];
            }
            lock (synchHelper)
            {
                if (propertyAccessors.ContainsKey(key))
                {
                    return propertyAccessors[key];
                }
                var propertyAccessor = new PropertyAccessor(targetType, propertyName);
                propertyAccessors[key] = propertyAccessor;
                return propertyAccessor;
            }
        }
        #endregion
    }
}